package com.vclee.fast.utils.sdk;

import com.vclee.fast.utils.CipherUtils;
import com.vclee.fast.utils.JwtUtils;
import com.vclee.fast.utils.SnowUtils;
import com.vclee.fast.utils.exception.Assert;
import com.vclee.fast.utils.model.Bson;
import com.vclee.fast.utils.model.WxPayReq;
import com.vclee.fast.utils.task.CuxExecutors;
import lombok.SneakyThrows;
import org.apache.http.HttpEntity;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.util.EntityUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.ResponseEntity;
import org.springframework.http.converter.StringHttpMessageConverter;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;

import javax.crypto.Cipher;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.spec.SecretKeySpec;
import javax.crypto.spec.GCMParameterSpec;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import java.io.IOException;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.math.BigDecimal;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.security.*;
import java.security.spec.PKCS8EncodedKeySpec;
import java.time.LocalDateTime;
import java.util.*;
import java.util.concurrent.ThreadPoolExecutor;


public class WxUtil {

    private static final Logger logger = LoggerFactory.getLogger(WxUtil.class);
    private final static RestTemplate restTemplate = new RestTemplate();

    static {
        restTemplate.getMessageConverters().add(0, new StringHttpMessageConverter(StandardCharsets.UTF_8));
    }

    private final static ThreadPoolExecutor executor = CuxExecutors.newSingleFixedThreadPool("WxUtil");

    //*****************************************A**************************************************
    //**************************************微信小程序模块**********************************************
    //*****************************************A**************************************************
    /**
     * 微信小程序订单发货录入
     *
     * @param openId         小程序openid
     * @param mchId          商户号码
     * @param appid          小程序appid
     * @param secret         小程序secret
     * @param orderNo        商户系统内部订单号
     * @param trackingNo     物流单号
     * @param expressCompany 快递公司ID 通过接口获取
     * @param itemDesc       商品信息(订单名字)
     *                       <a href="https://developers.weixin.qq.com/miniprogram/dev/platform-capabilities/business-capabilities/order-shipping/order-shipping.html#%E4%B8%80%E3%80%81%E5%8F%91%E8%B4%A7%E4%BF%A1%E6%81%AF%E5%BD%95%E5%85%A5%E6%8E%A5%E5%8F%A3">...</a>
     */
    public static void reportLogistics(String openId, String appid, String secret, String mchId, String orderNo, String trackingNo, String expressCompany, String itemDesc) {
        Bson params = new Bson();
        Bson orderKey = new Bson();
        orderKey.put("order_number_type", 1); // 1 使用transaction_id 2 不用transaction_id
//        orderKey.put("transaction_id",orderNo);
        orderKey.put("mchid", mchId);
        orderKey.put("out_trade_no", orderNo);
        params.put("order_key", orderKey);
        params.put("logistics_type", 1);
        params.put("delivery_mode", 1);
        Bson shipping = new Bson();
        shipping.put("tracking_no", trackingNo);
        shipping.put("express_company", expressCompany);
        shipping.put("item_desc", itemDesc);
        params.put("shipping_list", Collections.singletonList(shipping));
        params.put("upload_time", LocalDateTime.now() + "+08:00");
        Bson payer = new Bson();
        payer.put("openid", openId);
        params.put("payer", payer);
        String url = "https://api.weixin.qq.com/wxa/sec/order/upload_shipping_info?access_token=" + getToken(appid, secret);
        String res = restTemplate.postForObject(url, params, String.class);
        logger.warn("请求发货信息:" + res);
    }


    /**
     * 获取微信体系的token 公众号 小程序都可以通用
     * 调用该接口需要在微信公众平台上配置先配置白名单(白名单一般刚配置好存在一定时间延迟)
     * <a href="https://developers.weixin.qq.com/miniprogram/dev/OpenApiDoc/mp-access-token/getStableAccessToken.html">接口文档</a>
     */
    private static String getToken(String appid, String secret) {
        String url = "https://api.weixin.qq.com/cgi-bin/stable_token";
        Bson params = new Bson();
        params.put("grant_type", "client_credential");
        params.put("appid", appid);
        params.put("secret", secret);
        String res = restTemplate.postForObject(url, params, String.class);
        Bson json = Bson.create(res);
        return json.getString("access_token");
    }

    /**
     * 公众号推送模板消息
     *
     * @param openId     推送的OPENID
     * @param templateId 微信申请的模板ID
     * @param path       模板跳转的HTTP链接（海外帐号没有跳转能力） 或小程序路径
     * @param appid      模板跳转的小程序appid appid存在 path为小程序路径
     * @param content    模板消息所需要的参数key为模板值，value为实际值
     *                   <a href="https://developers.weixin.qq.com/doc/offiaccount/Message_Management/Template_Message_Interface.html#%E5%8F%91%E9%80%81%E6%A8%A1%E6%9D%BF%E6%B6%88%E6%81%AF">...</a>
     */
    private static void sendWxGzTpl(String openId, String gzhAppid, String gzhSecret, String templateId, String appid, String path, Map<String, String> content) {
        executor.execute(() -> {
            try {
                String req = "https://api.weixin.qq.com/cgi-bin/message/template/send?access_token=" + getToken(gzhAppid, gzhSecret);
                Bson params = new Bson();
                params.put("touser", openId);
                params.put("template_id", templateId);
                if (appid != null) {
                    Bson json = new Bson();
                    json.put("appid", appid);
                    json.put("pagepath", path);
                    params.put("miniprogram", json);
                } else {
                    params.put("url", path); // 模板跳转链接（海外帐号没有跳转能力）
                }

                Map<String, Bson> data = new HashMap<>();
                for (Map.Entry<String, String> entry : content.entrySet()) {
                    Bson json = new Bson();
                    json.put("value", entry.getValue());
                    data.put(entry.getKey(), json);
                }
                params.put("data", data);
                String res = restTemplate.postForObject(req, params, String.class);
                logger.info("推送微信公众号模板消息,请求地址:{},请求参数:{},响应结果:{}", req, params, res);
            } catch (Exception ignored) {
            }
        });
    }


    /**
     * 创建小程序通用二维码(推荐使用，无限制个数)
     *
     * @param scene  参数最大32个可见字符，只支持数字，大小写英文以及部分特殊字符：!#$&'()*+,/:;=?@-._~
     * @param page   小程序页面
     * @param width  二维码的宽度，单位 px，最小 280px，最大 1280px 默认430
     * @param appid  小程序appid
     * @param secret 小程序密钥
     */
    @SneakyThrows
    public static void createAppletQrCode(String scene, String page, String width, String appid, String secret) {
        HttpServletResponse response = JwtUtils.getContextHttpServletResponse();
        String url = "https://api.weixin.qq.com/wxa/getwxacodeunlimit?access_token=" + getToken(appid, secret);
        Bson params = new Bson();
        params.put("scene", scene); // 最大32个可见字符，只支持数字，大小写英文以及部分特殊字符：!#$&'()*+,/:;=?@-._~
        params.put("page", page); // 小程序页面 page，例如 pages/index/index，根路径前不要填加 /，不能携带参数（参数请放在scene字段里），如果不填写这个字段，默认跳主页面
        if (Assert.notBlank(width)) {
            params.put("width", width); // 二维码的宽度，单位 px，最小 280px，最大 1280px 默认430
        }
        ResponseEntity<byte[]> resData = restTemplate.postForEntity(url, params, byte[].class);
        ServletOutputStream outputStream = Objects.requireNonNull(response).getOutputStream();
        outputStream.write(Objects.requireNonNull(resData.getBody()));
        outputStream.close();
    }


    /**
     * 小程序登录
     *
     * @param code   登录时获取的 code，通过wx.login获取
     * @param appid  小程序appid
     * @param secret 参考文档 <a href="https://developers.weixin.qq.com/miniprogram/dev/OpenApiDoc/user-login/code2Session.html">...</a>
     *               返回参数 包含 openid 和 session_key 及 unionid 的json字符串
     */
    public static String wxAppletLogin(String code, String appid, String secret) {
        String url = "https://api.weixin.qq.com/sns/jscode2session?" +
                "appid=" + appid + "&secret=" + secret + "&js_code=" + code + "&grant_type=authorization_code";
        return restTemplate.getForObject(url, String.class);
    }

    /**
     * 小程序端获取用户手机号
     *
     * @param code   手机号获取凭证
     * @param appid  小程序appid
     * @param secret 参考文档 <a href="https://developers.weixin.qq.com/miniprogram/dev/OpenApiDoc/user-info/phone-number/getPhoneNumber.html">...</a>
     *               包含有用户绑定的手机号信息 phoneNumber
     */
    public static String wxAppletGetMobile(String code, String appid, String secret) {
        String url = "https://api.weixin.qq.com/wxa/business/getuserphonenumber?access_token=" + getToken(appid, secret);
        Bson params = new Bson();
        params.put("code", code);
        return restTemplate.postForObject(url, params, String.class);
    }

    //*****************************************A**************************************************
    //**************************************微信支付模块**********************************************
    //*****************************************A**************************************************

    /**
     * 微信支付v2版本回调通知处理
     * <a href="https://pay.weixin.qq.com/doc/v3/merchant/4012084686">...</a>
     */
    public static Bson wxPayV2Notify(String mchKey) {
        Bson result = new Bson();
        try {
            result.put("wxRes", "<xml><return_code><![CDATA[SUCCESS]]></return_code><return_msg><![CDATA[OK]]></return_msg></xml>");// 响应微信结果参数
            Bson map = handReqData();// 获取到document里面的全部结点
            logger.info("微信支付回调通知参数:" + map);
            String signature = map.getString("sign");
            String data = CipherUtils.formatUrlMap(map, false, true);
            String sign = CipherUtils.MD5(data + mchKey);
            if ("SUCCESS".equals(map.get("return_code")) && "SUCCESS".equals(map.get("result_code"))) {
                String ordNo = map.getString("out_trade_no"); // 商户系统内部订单号，要求32个字符内，只能是数字、大小写字母
                String wxOrdNo = map.getString("transaction_id"); // NATIVE：扫码支付 App：App支付 MICROPAY：付款码支付 MWEB：H5支付 FACEPAY：刷脸支付
                String tradeTime = map.getString("time_end"); // 支付完成时间，格式为yyyyMMddHHmmss，如2009年12月25日9点10分10秒表示为20091225091010
                String attach = map.getString("attach"); // 商家数据包,原样返回
                String tradeType = map.getString("trade_type"); // 交易类型 eg:JSAPI、PI、NATIVE、APP
                String total = String.valueOf(Double.parseDouble(map.getString("total_fee")) / 100); // 订单总金额，单位为分（55.22元为5522）
                logger.info("微信支付通知订单号为:" + ordNo + ",外部单号为:" + wxOrdNo + ",订单金额为:" + total + ",交易时间:" + tradeTime);
            }
        } catch (Exception ignored) {
        }
        return result;
    }
    @SneakyThrows
    public static Bson handReqData() {
        Bson map = new Bson();
        HttpServletRequest request = JwtUtils.getContextHttpServletRequest();
        DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
        DocumentBuilder builder = factory.newDocumentBuilder();
        Document document = builder.parse(Objects.requireNonNull(request).getInputStream());
        NodeList allNodes = document.getFirstChild().getChildNodes();
        Node node;
        int i = 0;
        while (i < allNodes.getLength()) {
            node = allNodes.item(i);
            if (node instanceof Element) map.put(node.getNodeName(), node.getTextContent());
            i++;
        }
        return map;
    }

    /**
     * 微信支付v3版本回调通知处理 直接使用post方法RequestBody中使用Bson接收即可调用该方法
     * 调用后将wxRes参数返回给微信服务器，微信服务器会自动判断是否成功，如果失败会重发通知，直到成功为止。
     * <a href="https://pay.weixin.qq.com/doc/v3/merchant/4012084686">...</a>
     * callback 参数eg说明
     * {"transaction_id":"4200002510202412037612811781","amount":{"total":1,"payer_total":1,"currency":"CNY","payer_currency":"CNY"},"mchid":"1696681777","out_trade_no":"oZ5uQ7dKOzNbABx3X0bmxkAmRSYY","trade_state":"SUCCESS","bank_type":"CITIC_CREDIT","appid":"wxd7029c6a261ef094","trade_state_desc":"支付成功","trade_type":"JSAPI","attach":"c5e92aef-bde2-48e8-ab4f-040b66a5e110","success_time":"2024-12-03T16:25:29+08:00","payer":{"openid":"oZ5uQ7dKOzNbABx3X0bmxkAmRSYY"}}
     */
    @SneakyThrows
    public static Bson wxPayV3Notify(Bson params, String mchV3Key) {
        Bson result = new Bson();
        try {
            Bson wxRes = new Bson();
            wxRes.put("CODE", "SUCCESS");
            result.put("wxRes", wxRes);
            fixKeyLength(); // 解决aes长度限制问题
            Bson resource = params.getBson("resource");
            String ciphertext = resource.getString("ciphertext");
            String nonce = resource.getString("nonce");
            String associatedData = resource.getString("associated_data");
            String decrypt = decryptToString(associatedData, nonce, ciphertext, mchV3Key);
            result.put("callback", Bson.create(decrypt));
        } catch (Exception ignored) {
        }
        return result;
    }

    private static String decryptToString(String associatedData, String nonce, String ciphertext, String aesKey) throws GeneralSecurityException {
        try {
            Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
            SecretKeySpec key = new SecretKeySpec(aesKey.getBytes(), "AES");
            GCMParameterSpec spec = new GCMParameterSpec(128, nonce.getBytes());
            cipher.init(Cipher.DECRYPT_MODE, key, spec);
            cipher.updateAAD(associatedData.getBytes());
            return new String(cipher.doFinal(Base64.getDecoder().decode(ciphertext)), StandardCharsets.UTF_8);
        } catch (NoSuchAlgorithmException | NoSuchPaddingException e) {
            throw new IllegalStateException(e);
        } catch (InvalidKeyException | InvalidAlgorithmParameterException e) {
            throw new IllegalArgumentException(e);
        }
    }

    @SuppressWarnings("all")
    private static void fixKeyLength() {
        String errorString = "Failed manually overriding key-length permissions.";
        int newMaxKeyLength;
        try {
            if ((newMaxKeyLength = Cipher.getMaxAllowedKeyLength("AES")) < 256) {
                Class c = Class.forName("javax.crypto.CryptoAllPermissionCollection");
                Constructor con = c.getDeclaredConstructor();
                con.setAccessible(true);
                Object allPermissionCollection = con.newInstance();
                Field f = c.getDeclaredField("all_allowed");
                f.setAccessible(true);
                f.setBoolean(allPermissionCollection, true);

                c = Class.forName("javax.crypto.CryptoPermissions");
                con = c.getDeclaredConstructor();
                con.setAccessible(true);
                Object allPermissions = con.newInstance();
                f = c.getDeclaredField("perms");
                f.setAccessible(true);
                ((Map) f.get(allPermissions)).put("*", allPermissionCollection);

                c = Class.forName("javax.crypto.JceSecurityManager");
                f = c.getDeclaredField("defaultPolicy");
                f.setAccessible(true);
                Field mf = Field.class.getDeclaredField("modifiers");
                mf.setAccessible(true);
                mf.setInt(f, f.getModifiers() & ~Modifier.FINAL);
                f.set(null, allPermissions);
                newMaxKeyLength = Cipher.getMaxAllowedKeyLength("AES");
            }
        } catch (Exception e) {
            throw new RuntimeException(errorString, e);
        }
        if (newMaxKeyLength < 256) throw new RuntimeException(errorString); // hack failed
    }

    /**
     * 微信支付v3版本
     * 参考文档 <a href="https://pay.weixin.qq.com/doc/v3/merchant/4012525110">...</a>
     */
    public static Object wxPayV3CreateAppletOrder(WxPayReq req) {
        Bson params = getParams(req);
        String url = "/v3/pay/transactions/jsapi";
        String result = postV3PayRequest(url, params.toString(), req.getMchId(), req.getApiV3MchNo(), req.getApiV3MchCertPath());
        logger.info("微信支付下单,请求参数:{},结果:{}", params, result);
        if (req.getPayType() == 1) {
            String prepayId = Bson.create(result).getString("prepay_id");
            String timestamp = String.valueOf(System.currentTimeMillis() / 1000);
            String nonceStr = SnowUtils.getStrKey();
            String prepay = "prepay_id=" + prepayId;
            String message = req.getAppid() + "\n" + timestamp + "\n" + nonceStr + "\n" + prepay + "\n";
            Bson payParams = new Bson();
            payParams.put("timeStamp", timestamp);
            payParams.put("nonceStr", nonceStr);
            payParams.put("signType", "RSA");
            payParams.put("package", prepay);
            payParams.put("paySign", getSignature(req.getApiV3MchCertPath(), message));
            return payParams;
        }
        // TODO 功能尚未完成 后续需要支持APP h5 二维码三种类型支付
        return null;
    }

    private static Bson getParams(WxPayReq req) {
        Bson params = new Bson();
        params.put("appid", req.getAppid());
        params.put("mchid", req.getMchId());
        params.put("description", req.getTitle());
        params.put("out_trade_no", req.getOpenId());
        params.put("notify_url", req.getCallbackUrl());
        if (Assert.notBlank(req.getAttach())) {
            params.put("attach", req.getAttach());
        }
        Bson amount = new Bson();
        amount.put("total", req.getPrice().multiply(new BigDecimal("100")).intValue());
        params.put("amount", amount);
        Bson payer = new Bson();
        payer.put("openid", req.getOpenId());
        params.put("payer", payer);
        return params;
    }


    /**
     * 微信退款
     *
     * @param orderNo 订单号  eg: MN202112211234567890
     * @param price   退款金额  eg: 0.01 退款金额不能超过订单支付总金额
     * @param total   订单支付总金额  eg: 0.01
     * @param reason  退款原因 eg: 款式不同
     * @param mchId   商户号 eg: 1501121711
     * @param certSn  绑定的商户证书序列号 eg: 45F07A25867EF69317D5B78D0B39C091769C94D1
     * @param pkPath  绑定的商户证书私钥路径 eg: D:\zil\cert\api_key.pem
     */
    public static void refund(String orderNo, BigDecimal price, BigDecimal total, String reason,
                              String mchId, String certSn, String pkPath) {
        Bson params = new Bson();
        params.put("out_trade_no", orderNo);
        params.put("out_refund_no", SnowUtils.key32Str());
        params.put("reason", reason);
        Bson amount = new Bson();
        amount.put("refund", price.multiply(new BigDecimal("100")).intValue());
        amount.put("total", total.multiply(new BigDecimal("100")).intValue());
        amount.put("currency", "CNY");
        params.put("amount", amount);
        String url = "/v3/refund/domestic/refunds";
        String result = postV3PayRequest(url, params.toString(), mchId, certSn, pkPath);
        logger.info("退款请求参数:{},结果:{},mchId:{},certSn:{},pkPath:{}", params, result, mchId, certSn, pkPath);
    }


    /**
     * 微信零钱转账(最高支持1999.99元) 使用的是运营账户进行转账打款 与微信支付账户独立 需要充值
     *
     * @param amount 转账金额 最多支持2位小数 即到分
     * @param openId 对应程序openId(可以来源于小程序,公众号,开放平台) eg oW6E45AiCS8s5h_QlQRgTRG6CxkA
     * @param appid  对应程序appid(可以来源于小程序,公众号,开放平台) eg wx1cd21277baf4ed08
     * @param mchId  绑定的商户号  eg: 1501121711
     * @param certSn 绑定的商户证书序列号 eg: 45F07A25867EF69317D5B78D0B39C091769C94D1
     * @param pkPath 绑定的商户证书私钥路径 eg: D:\zil\cert\api_key.pem
     */
    public static void transfer(BigDecimal amount, String openId, String appid, String mchId, String certSn, String pkPath) {
        String outNo = SnowUtils.getStrKey();
        String batchName = "微信零钱转账";
        Bson params = new Bson();
        params.put("appid", appid);//绑定支付的appid
        params.put("out_batch_no", outNo); // 商户订单号
        params.put("batch_name", batchName); // 该笔批量转账的名称
        params.put("batch_remark", batchName); //转账说明，UTF8编码，最多允许32个字符
        int centAmount = amount.multiply(new BigDecimal("100")).intValue();
        params.put("total_amount", centAmount); //转账金额单位为“分”。 总金额
        params.put("total_num", 1); //转账总笔数
        List<Map<String, Object>> list = new ArrayList<>();
        Map<String, Object> subMap = new HashMap<>(4);
        subMap.put("out_detail_no", outNo);//商家明细单号 该商家下唯一
        subMap.put("transfer_amount", centAmount);//转账金额单位为“分”
        subMap.put("transfer_remark", batchName);//转账备注
        subMap.put("openid", openId);//用户在直连商户应用下的用户标示
        //大金额需要传入真实姓名(收款方姓名 >=2000必须要传递)
        // subMap.put("user_name", RsaCryptoUtil.encryptOAEP(userName,WechatPayV3Util.getSaveCertificates(pkPath)));
        list.add(subMap);
        params.put("transfer_detail_list", list);
        String result = postV3PayRequest("/v3/transfer/batches", params.toString(), mchId, certSn, pkPath);
        logger.info("微信转账请求参数:{},结果：{},mchId:{},certSn:{},pkPath:{}", params, result, mchId, certSn, pkPath);
    }


    /**
     * 微信支付V3版本通用请求
     *
     * @param url    相对路径 eg /v3/transfer/batches
     * @param json   请求参数 json字符串格式
     * @param mchId  商户号 eg: 1501121711
     * @param certSn 商户证书序列号 eg: 45F07A25867EF69317D5B78D0B39C091769C94D1
     * @param pkPath 商户私钥证书路径 eg: D:\zil\cert\apiclient_key.pem
     */
    @SneakyThrows
    private static String postV3PayRequest(String url, String json, String mchId, String certSn, String pkPath) {
        String BASE_URL = "https://api.mch.weixin.qq.com";
        CloseableHttpClient client = HttpClients.createDefault();
        HttpPost httpPost = new HttpPost(BASE_URL + url);
        // NOTE: 建议指定charset=utf-8。低于4.4.6版本的HttpCore，不能正确的设置字符集，可能导致签名错误
        httpPost.addHeader("Content-Type", "application/json");
        httpPost.addHeader("Accept", "application/json");
        httpPost.addHeader("Wechatpay-Serial", certSn);
        Random random = new Random();
        StringBuilder sb = new StringBuilder();
        String base = "9abc0def1ghi2jkl3mno4pqr5stu6vwx7yz8";// 签名处理
        for (int i = 0; i < 32; i++) sb.append(base.charAt(random.nextInt(base.length())));
        String nonceStr = sb.toString();
        long timestamp = System.currentTimeMillis() / 1000;
        String message = "POST\n" + url + "\n" + timestamp + "\n" + nonceStr + "\n" + json + "\n";
        String signature = getSignature(pkPath, message);
        String token = "mchid=\"" + mchId + "\",timestamp=\"" + timestamp + "\",nonce_str=\"" + nonceStr
                + "\",serial_no=\"" + certSn + "\",signature=\"" + signature + "\"";
        httpPost.addHeader("Authorization", "WECHATPAY2-SHA256-RSA2048" + " " + token);
        // 请求参数
        httpPost.setEntity(new StringEntity(json, "UTF-8"));
        CloseableHttpResponse response = client.execute(httpPost);
        HttpEntity entity = response.getEntity();//获取返回的数据
        client.close();
        return EntityUtils.toString(entity);
    }

    @SneakyThrows
    private static String getSignature(String pkPath, String message) {
        Signature sign = Signature.getInstance("SHA256withRSA");
        String content = new String(Files.readAllBytes(Paths.get(pkPath)), StandardCharsets.UTF_8);
        String privateKey = content.replace("-----BEGIN PRIVATE KEY-----", "")
                .replace("-----END PRIVATE KEY-----", "")
                .replaceAll("\\s+", "");
        KeyFactory kf = KeyFactory.getInstance("RSA");
        PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(Base64.getDecoder().decode(privateKey));
        sign.initSign(kf.generatePrivate(keySpec));
        sign.update(message.getBytes(StandardCharsets.UTF_8));
        return Base64.getEncoder().encodeToString(sign.sign());
    }


    //*****************************************A**************************************************
    //**************************************公众号模块**********************************************
    //*****************************************A**************************************************

    /**
     * @param code      扫码后得到的code(场景值ID（字符串形式的ID），字符串类型，长度限制为1到64)
     * @param gzhAppid  公众号appid
     * @param gzhSecret 公众号secret
     *                  参考文档 <a href="https://developers.weixin.qq.com/doc/offiaccount/Account_Management/Generating_a_Parametric_QR_Code.html">...</a>
     */
    @SneakyThrows
    public static void getGzhQrCode(String code, String gzhAppid, String gzhSecret) {
        String url = "https://api.weixin.qq.com/cgi-bin/qrcode/create?access_token=" + getToken(gzhAppid, gzhSecret);
        HttpServletResponse response = JwtUtils.getContextHttpServletResponse();
        // 永久二维码参数 {"action_name": "QR_LIMIT_STR_SCENE", "action_info": {"scene": {"scene_str": "test"}}}
        Bson params = new Bson();
        params.put("expire_seconds", 604800); // 最大2592000秒（即30天）
        params.put("action_name", "QR_STR_SCENE");
        Bson sceneStr = new Bson();
        sceneStr.put("scene_str", code); // 场景值ID（字符串形式的ID），字符串类型，长度限制为1到64
        Bson data = new Bson();
        data.put("scene", sceneStr);
        params.put("action_info", data);
        String res = restTemplate.postForEntity(url, params, String.class).getBody();
        Bson result = Bson.create(res);
        String ticket = result.getString("ticket");
        String qrCodeUrl = "https://mp.weixin.qq.com/cgi-bin/showqrcode?ticket=" + ticket;
        ServletOutputStream outputStream = Objects.requireNonNull(response).getOutputStream();
        outputStream.write(Objects.requireNonNull(restTemplate.getForEntity(qrCodeUrl, byte[].class).getBody()));
        outputStream.close();
    }


    /**
     * 获取公众号对应的UNIONID(只有公众号绑定到微信开放平台账号后，才会出现该字段。)
     *
     * @param openId    公众号用户的openId
     * @param gzhAppid  公众号appid
     * @param gzhSecret 公众号secret
     *                  <a href="https://developers.weixin.qq.com/doc/offiaccount/User_Management/Get_users_basic_information_UnionID.html#UinonId">...</a>
     */
    public static String getGzhUnionId(String openId, String gzhAppid, String gzhSecret) {
        String url = "https://api.weixin.qq.com/cgi-bin/user/info?access_token=" + getToken(gzhAppid, gzhSecret) + "&openid=" + openId + "&lang=zh_CN";
        return Bson.create(restTemplate.getForObject(url, String.class)).getString("unionid");
    }

    /**
     * 公众号客服消息(触发式消息,用户主动触发后，48小时内可以推送五条消息),支持富文本
     * 支持插入跳小程序的文字链 <a href="http://www.qq.com" data-miniprogram-appid="appid" data-miniprogram-path="pages/index/index">点击跳小程序</a>
     * <a href="https://developers.weixin.qq.com/doc/offiaccount/Message_Management/Service_Center_messages.html#%E5%AE%A2%E6%9C%8D%E6%8E%A5%E5%8F%A3-%E5%8F%91%E6%B6%88%E6%81%AF">...</a>
     * @param openId openId
     * @param text 文本
     * @param appid 公众号appid
     * @param appSecret 公众号密钥
     */
    public static void kfTextMsg(String openId, String text,String appid,String appSecret) {
        List<String> parts = new ArrayList<>();
        int length = text.length();
        Assert.claim(length <= 650*5, "文本长度不能超过3200");
        for (int i = 0; i < length; i += 650) {
            parts.add(text.substring(i, Math.min(length, i + 650)));
        }
        for (String part : parts) {
            String url = "https://api.weixin.qq.com/cgi-bin/message/custom/send?access_token="+getToken(appid,appSecret);
            Bson params = new Bson();
            params.put("touser", openId);
            params.put("msgtype", "text");
            Bson data = new Bson();
            data.put("content",part);
            params.put("text", data);
            String res = restTemplate.postForEntity(url, params, String.class).getBody();
            logger.info("推送消息,openId:{},结果：{},内容:{}",openId,res,text);
        }
    }

}
